﻿Option Explicit On
Option Strict On

Imports System.Data
Imports System.Text
Imports System.Globalization
Imports System.Reflection
Imports System.Dynamic


Namespace VSProject.MicroORM

#Region "       SQLContext.vb - Контекст       "

    ''' <summary>Класс предоставляет методы для CRUD операций с ORM составляющей</summary>
    Public Class SQLContext
        Implements IDisposable

        ''' <summary>Событие срабатывает после открытия соединения. Предоставляет объект соединения для использования в пользовательском коде, изменения PRAGMA, и т.д.</summary>
        Public Event OpenedConnection(Connection As IDbConnection)

        ''' <summary>Событие срабатывает перед закрытием соединения</summary>
        Public Event ClosingConnection(Connection As IDbConnection)

        ''' <summary>Строка соединения с базой данных</summary>
        Public Property ConnectionString As String

        ''' <summary>Соединение для внутреннего использования</summary>
        Private Connection As IDbConnection

        ''' <summary>Формат даты и времени по-умолчанию в формате ISO</summary>
        Private Const SQL_DATE_FORMAT As String = "yyyy-MM-dd HH:mm:ss"


#Region "       Конструкторы       "

        ''' <summary>Конструктор по-умолчанию</summary>
        Public Sub New()
        End Sub

        ''' <summary>Конструктор принимает строку соединения с базой данных. Провайдер SqlClient используется по-умолчанию, если не задан объект соединения</summary>
        Public Sub New(ConnectionString As String)
            Me.ConnectionString = ConnectionString
        End Sub

        ''' <summary>Конструктор принимает объект соединения, при этом строка соединения используется из объекта</summary>
        Public Sub New(Connection As IDbConnection)
            Me.Connection = Connection
            If Connection IsNot Nothing Then Me.ConnectionString = Connection.ConnectionString
        End Sub

#End Region

#Region "       Приватные методы для отражения и маппинга класса и полей таблиц       "

        ''' <summary>Открывает новое соединение или возвращает уже открытое</summary>
        Friend Function OpenConnection() As IDbConnection
            'Если соединение не задано, то создать (По умолчанию SqlClient)
            If Me.Connection Is Nothing Then
                Me.Connection = Data.SqlClient.SqlClientFactory.Instance.CreateConnection()
                Me.Connection.ConnectionString = Me.ConnectionString
            End If

            'Если строка соединения в свойстве отличается от строки в соединении - переоткрыть
            If Me.ConnectionString <> Me.Connection.ConnectionString Then
                Me.Connection.Close()
                Me.Connection.ConnectionString = Me.ConnectionString
            End If

            'Если в состоянии закрыто - открыть
            If Me.Connection.State = ConnectionState.Closed Then
                Me.Connection.Open()
                RaiseEvent OpenedConnection(Me.Connection)
            End If

            Return Connection
        End Function

        ''' <summary>Класс кеширует на время работы приложения типы для маппинга, кроме данных</summary>
        Friend Class CacheTypesDataInformation

            Private Shared mDictClass As New Dictionary(Of Type, DataInformation)

            Public Shared Sub Store(ByVal ClassType As Type, ByRef DI As DataInformation)
                If mDictClass.ContainsKey(ClassType) Then mDictClass.Remove(ClassType)

                Dim tmpDI As DataInformation = CloneDI(DI)
                tmpDI.ColumnValues.Clear()

                mDictClass.Add(ClassType, tmpDI)
            End Sub

            Public Shared Function Load(ByVal ClassType As Type, ByRef DI As DataInformation) As DataInformation
                If mDictClass.ContainsKey(ClassType) Then
                    Dim tmpDI As DataInformation = CloneDI(mDictClass(ClassType))
                    tmpDI.ColumnValues = DI.ColumnValues

                    Return tmpDI
                Else
                    Return Nothing
                End If
            End Function

            Public Shared Function Available(ByVal ClassType As Type) As Boolean
                Return mDictClass.ContainsKey(ClassType)
            End Function

            'Реализует клонирование типа DataInformation
            Private Shared Function CloneDI(ByRef srcDI As DataInformation) As DataInformation
                Dim dstDI As New DataInformation
                dstDI.TableNameInfo = srcDI.TableNameInfo

                'Копируем столбцы
                For Each xCI In srcDI.ColumnInfo
                    dstDI.ColumnInfo.Add(xCI)
                Next

                Return dstDI
            End Function

        End Class

        ''' <summary>Класс содержит информацию о таблице, её столбцах, атрибутах и данных таблицы</summary>
        Friend Class DataInformation

            Public TableNameInfo As String
            Public ColumnInfo As New List(Of ColumnInformation)
            Public ColumnValues As New Dictionary(Of String, Object)


            Public Function FindByColumnName(ByVal Name As String) As ColumnInformation
                Return Me.ColumnInfo.FirstOrDefault(Function(ci) ci.ColumnName.Equals(Name, StringComparison.InvariantCultureIgnoreCase))
            End Function

            Public Function FindByPropertyName(ByVal Name As String) As ColumnInformation
                Return Me.ColumnInfo.FirstOrDefault(Function(ci) ci.PropertyName.Equals(Name, StringComparison.InvariantCultureIgnoreCase))
            End Function
        End Class

        ''' <summary>Класс содержит метаданные для каждого столбца таблицы</summary>
        Friend Class ColumnInformation
            Public ColumnName As String
            Public ColumnType As StorageClass

            Public PropertyName As String
            Public PropertyType As String

            Public PrimaryKey As Boolean
            Public AutoIncrement As Boolean
            Public Programmable As Boolean
        End Class


        ''' <summary>Преобразует данные из DI в класс</summary>
        Friend Shared Function FromDataInfoToClass(Of TClass)(DataInfo As DataInformation) As TClass
            Dim xClassObject As TClass = Activator.CreateInstance(Of TClass)()
            Dim tpClass As Type = GetType(TClass)

            'Проверка в кеше
            If CacheTypesDataInformation.Available(tpClass) Then
                DataInfo = CacheTypesDataInformation.Load(tpClass, DataInfo) 'Сначала читаем из кеша
            Else
                'Дополняем информацию по метаданным, необходимую для корректного маппинга
                DataInfo = FindTableColumnsMetaInfo(xClassObject, DataInfo)

                CacheTypesDataInformation.Store(tpClass, DataInfo)
            End If

            'По всем свойствам
            For Each xClassProperty In tpClass.GetProperties(Reflection.BindingFlags.Instance Or Reflection.BindingFlags.Public)
                Dim ColumnInfo As ColumnInformation = DataInfo.FindByPropertyName(xClassProperty.Name)

                'Только для тех свойств, которые можно смаппить с названиями столбцов, остальные пропускаем
                If DataInfo.ColumnValues.ContainsKey(ColumnInfo.ColumnName) Then

                    If TypeOf DataInfo.ColumnValues(ColumnInfo.ColumnName) Is DBNull Then 'Не потерять NULL
                        xClassProperty.SetValue(xClassObject, Nothing, Nothing)
                    Else
                        Select Case ColumnInfo.ColumnType
                            Case StorageClass.DateTime
                                Dim IsTextDateTime = DateTime.TryParseExact(DataInfo.ColumnValues(ColumnInfo.ColumnName).ToString, SQL_DATE_FORMAT, Nothing, DateTimeStyles.None, Nothing)

                                'Если требуется преобразование (когда данные Текст, а нужен тип Дата)
                                If IsTextDateTime Then
                                    xClassProperty.SetValue(xClassObject, DateTime.ParseExact(DataInfo.ColumnValues(ColumnInfo.ColumnName).ToString, SQL_DATE_FORMAT, Nothing), Nothing)
                                Else
                                    xClassProperty.SetValue(xClassObject, DataInfo.ColumnValues(ColumnInfo.ColumnName), Nothing)
                                End If

                            Case StorageClass.Bool
                                Dim IsNumberBoolean = Integer.TryParse(DataInfo.ColumnValues(ColumnInfo.ColumnName).ToString, Nothing)
                                Dim IsTextBoolean = Boolean.TryParse(DataInfo.ColumnValues(ColumnInfo.ColumnName).ToString, Nothing)

                                'Если требуется преобразование (когда данные Текст, а нужен тип Булево)
                                If IsNumberBoolean Then
                                    xClassProperty.SetValue(xClassObject, If(Convert.ToInt32(DataInfo.ColumnValues(ColumnInfo.ColumnName), CultureInfo.InvariantCulture) = 1, True, False), Nothing)
                                Else
                                    'Если требуется преобразование (когда данные Число, а нужен тип Булево)
                                    If IsTextBoolean Then
                                        xClassProperty.SetValue(xClassObject, If(Convert.ToString(DataInfo.ColumnValues(ColumnInfo.ColumnName), CultureInfo.InvariantCulture).ToLower().Equals("true"), True, False), Nothing)
                                    Else
                                        xClassProperty.SetValue(xClassObject, DataInfo.ColumnValues(ColumnInfo.ColumnName), Nothing)
                                    End If
                                End If

                            Case StorageClass.Text, StorageClass.Integral, StorageClass.Fractional
                                xClassProperty.SetValue(xClassObject, DataInfo.ColumnValues(ColumnInfo.ColumnName), Nothing)

                        End Select
                    End If

                End If
            Next

            Return xClassObject
        End Function

        ''' <summary>Преобразует данные из класса в DI</summary>
        Friend Shared Function FromClassToDataInfo(Of TClass)(ClassObject As TClass) As DataInformation
            Dim DataInfo As New DataInformation
            Dim tpClass As Type = GetType(TClass)

            'Проверка в кеше
            If CacheTypesDataInformation.Available(tpClass) Then
                DataInfo = CacheTypesDataInformation.Load(tpClass, DataInfo) 'Сначала читаем из кеша
            Else
                'Дополняем информацию по метаданным, необходимую для корректного маппинга
                DataInfo = FindTableColumnsMetaInfo(ClassObject, DataInfo)

                CacheTypesDataInformation.Store(tpClass, DataInfo)
            End If

            'По всем свойствам
            For Each xClassProperty In tpClass.GetProperties(Reflection.BindingFlags.Instance Or Reflection.BindingFlags.Public)
                Dim ColumnInfo As ColumnInformation = DataInfo.FindByPropertyName(xClassProperty.Name)
                Dim ColumnValue As Object = xClassProperty.GetValue(ClassObject, Nothing)

                DataInfo.ColumnValues.Add(ColumnInfo.ColumnName, ColumnValue)
            Next

            Return DataInfo
        End Function

        ''' <summary>Преобразует данные из DI в объект DynamicObject</summary>
        Friend Function FromDataInfoToDynamic(DataInfo As DataInformation) As Object
            Dim ExpObject As New ExpandoObject()
            Dim ExpDict = DirectCast(ExpObject, IDictionary(Of String, Object))

            For Each xItem In DataInfo.ColumnValues
                If TypeOf xItem.Value Is DBNull Then
                    ExpDict.Add(xItem.Key, Nothing)
                Else
                    ExpDict.Add(xItem.Key, xItem.Value)
                End If
            Next

            Return ExpObject
        End Function

        ''' <summary>Найти в объекте атрибуты таблицы и столбцов</summary>
        Friend Shared Function FindTableColumnsMetaInfo(Of TClass)(ByRef ClassObject As TClass, ByRef SourceDI As DataInformation) As DataInformation
            'Создаём экземпляр, если его не было
            If SourceDI Is Nothing Then SourceDI = New DataInformation()

            'Поиск атрибута у класса
            For Each xAttrib As Attribute In GetType(TClass).GetCustomAttributes(GetType(TableAttribute), True)
                SourceDI.TableNameInfo = DirectCast(xAttrib, TableAttribute).Name
            Next

            'Если нет переопределения имени, то именем таблицы будет название класса
            If String.IsNullOrEmpty(SourceDI.TableNameInfo) Then SourceDI.TableNameInfo = GetType(TClass).Name

            'Добавляем найденные атрибуты. Если атрибут не указан, используется значение атрибута по-умолчанию
            For Each xClassProperty In GetType(TClass).GetProperties(Reflection.BindingFlags.Instance Or Reflection.BindingFlags.Public)
                Dim ItemDC As New ColumnInformation

                ItemDC.PropertyName = xClassProperty.Name
                ItemDC.PropertyType = ValueTypeFrom(xClassProperty).Name

                For Each xAttribute In xClassProperty.GetCustomAttributes(True)
                    If TypeOf xAttribute Is ColumnAttribute Then ItemDC.ColumnName = DirectCast(xAttribute, ColumnAttribute).Name
                    If TypeOf xAttribute Is StorageAttribute Then ItemDC.ColumnType = DirectCast(xAttribute, StorageAttribute).ColumnType
                    If TypeOf xAttribute Is PrimaryKeyAttribute Then ItemDC.PrimaryKey = True
                    If TypeOf xAttribute Is AutoincrementAttribute Then ItemDC.AutoIncrement = True
                    If TypeOf xAttribute Is ProgrammableAttribute Then ItemDC.Programmable = True
                Next

                'Если название для столбца не было задано через аттрибуты, используем тогда имя свойства
                If String.IsNullOrEmpty(ItemDC.ColumnName) Then ItemDC.ColumnName = xClassProperty.Name

                'Если нет информации о типе столбца, брать информацию из типа свойства
                If ItemDC.ColumnType = StorageClass.Auto Then
                    Select Case ItemDC.PropertyType
                        Case "Byte", "Int16", "Int32", "Int64", "Decimal" : ItemDC.ColumnType = StorageClass.Integral
                        Case "SByte", "UInt16", "UInt32", "UInt64" : ItemDC.ColumnType = StorageClass.Integral
                        Case "Single", "Double" : ItemDC.ColumnType = StorageClass.Fractional
                        Case "String" : ItemDC.ColumnType = StorageClass.Text
                        Case "DateTime" : ItemDC.ColumnType = StorageClass.DateTime
                        Case "Boolean" : ItemDC.ColumnType = StorageClass.Bool
                        Case Else : ItemDC.ColumnType = StorageClass.Text
                    End Select
                End If

                SourceDI.ColumnInfo.Add(ItemDC)
            Next

            'Возвращаем
            Return SourceDI
        End Function

        ''' <summary>Метод возвращает значимый тип по свойству, извлекая информацию из Nullable типа</summary>
        Private Shared Function ValueTypeFrom(propertyInfo As PropertyInfo) As Type
            Return If(IsNullableType(propertyInfo), Nullable.GetUnderlyingType(propertyInfo.PropertyType), propertyInfo.PropertyType)
        End Function

        ''' <summary>Метод проверяет, имеет ли свойство тип Nullable</summary>
        Friend Shared Function IsNullableType(propertyInfo As PropertyInfo) As Boolean
            Return propertyInfo.PropertyType IsNot Nothing AndAlso propertyInfo.PropertyType.IsGenericType AndAlso propertyInfo.PropertyType.GetGenericTypeDefinition() = GetType(Nullable(Of ))
        End Function

#End Region


        ''' <summary>Выборка данных по SQL запросу, возвращает перечисление значимых типов IEnumerable(Of Value) или проекцию на класс IEnumerable(Of TClass)</summary>
        ''' <param name="SQLText">Текст запроса SQL</param>
        Public Iterator Function SelectData(Of TClass)(ByVal SQLText As String) As IEnumerable(Of TClass)
            Try
                Dim SqlCn As IDbConnection = OpenConnection()
                Dim tpClass As Type = GetType(TClass)

                '# Создаём команду с запросом
                Using SqlCmd As IDbCommand = SqlCn.CreateCommand()
                    SqlCmd.CommandType = CommandType.Text
                    SqlCmd.CommandText = SQLText

                    Using SqlRead As IDataReader = SqlCmd.ExecuteReader()

                        While SqlRead.Read()
                            Dim objValues(SqlRead.FieldCount - 1) As Object
                            SqlRead.GetValues(objValues)

                            'Для ссылочных и значимых типов используется разный маппер
                            Select Case tpClass.IsNested
                                Case True
                                    Dim DI As New DataInformation

                                    For I = 0 To SqlRead.FieldCount - 1
                                        DI.ColumnValues(SqlRead.GetName(I)) = objValues(I)
                                    Next

                                    Yield FromDataInfoToClass(Of TClass)(DI)

                                Case False
                                    Yield DirectCast(objValues(0), TClass)

                            End Select
                        End While

                    End Using
                End Using
            Catch ex As Exception
                Throw New SQLContextUnknownException(ex.Message)
            End Try
        End Function

        ''' <summary>Выборка данных по SQL запросу, возвращает IEnumerable(Of DynamicObject)</summary>
        ''' <param name="SQLText">Текст запроса SQL</param>
        Public Iterator Function SelectData(ByVal SQLText As String) As IEnumerable(Of Object)
            Dim SqlCn As IDbConnection = OpenConnection()

            '# Создаём команду с запросом
            Using SqlCmd As IDbCommand = SqlCn.CreateCommand()
                SqlCmd.CommandType = CommandType.Text
                SqlCmd.CommandText = SQLText

                Using SqlRead As IDataReader = SqlCmd.ExecuteReader()

                    While SqlRead.Read()
                        Dim objValues(SqlRead.FieldCount - 1) As Object
                        SqlRead.GetValues(objValues)

                        Dim DI As New DataInformation

                        For I = 0 To SqlRead.FieldCount - 1
                            DI.ColumnValues(SqlRead.GetName(I)) = objValues(I)
                        Next

                        Yield FromDataInfoToDynamic(DI)
                    End While

                End Using
            End Using
        End Function

        ''' <summary>Вставка данных на основе данных класса. Свойства с атрибутом AutoIncrement и нулевым значением не задействованы в операциях вставки</summary>
        Public Function InsertData(Of TClass)(Data As TClass) As Integer
            Dim DI As DataInformation = FromClassToDataInfo(Data)

            Dim strColumnNames As New StringBuilder
            Dim strColumnValues As New StringBuilder

            'Генерируем запрос вставки
            For Each xColumn In DI.ColumnInfo

                'Пропуск поля если оно автоинкремент и значение равно 0, иначе используем в запросе
                'Также пропускаем, если указан атрибут "программируемый"
                Dim xColumnValue = DI.ColumnValues(xColumn.ColumnName)
                If (xColumn.AutoIncrement AndAlso CLng(xColumnValue) = 0L) OrElse xColumn.Programmable Then Continue For

                strColumnNames.Append("[")
                strColumnNames.Append(xColumn.ColumnName)
                strColumnNames.Append("],")

                If xColumnValue Is Nothing Then
                    strColumnValues.Append("null,")
                Else
                    Select Case xColumn.ColumnType
                        Case StorageClass.Text
                            strColumnValues.Append("'")
                            strColumnValues.Append(xColumnValue.ToString())
                            strColumnValues.Append("',")

                        Case StorageClass.Integral, StorageClass.Fractional
                            strColumnValues.Append(xColumnValue.ToString())
                            strColumnValues.Append(",")

                        Case StorageClass.Bool
                            strColumnValues.Append("'")
                            strColumnValues.Append(Convert.ToBoolean(xColumnValue).ToString().ToLowerInvariant())
                            strColumnValues.Append("',")

                        Case StorageClass.DateTime
                            strColumnValues.Append("'")
                            strColumnValues.Append(Convert.ToDateTime(xColumnValue).ToString(SQL_DATE_FORMAT))
                            strColumnValues.Append("',")
                    End Select
                End If
            Next

            If strColumnNames.Length > 0 Then
                strColumnNames.Remove(strColumnNames.Length - 1, 1)
                strColumnValues.Remove(strColumnValues.Length - 1, 1)
            Else
                Throw New SQLColumnsNotFoundException("Нет столбцов или имеется только столбец первичного ключа. Вставка невозможна в режиме автоинкремента в такие таблицы")
            End If

            'Выполняем запрос к базе, создав запрос
            Dim SqlCn As IDbConnection = OpenConnection()
            Dim SqlCmd As IDbCommand = SqlCn.CreateCommand()

            SqlCmd.CommandText = String.Format("insert into [{0}]({1}) values({2})", DI.TableNameInfo, strColumnNames.ToString(), strColumnValues.ToString())
            Dim retInt As Integer = SqlCmd.ExecuteNonQuery()

            Return retInt
        End Function

        ''' <summary>Обновление данных на основе данных класса. По свойству с атрибутом PrimaryKey происходит выборка обновляемых данных</summary>
        Public Function UpdateData(Of TClass)(Data As TClass) As Integer
            Dim DI = FromClassToDataInfo(Data)

            'Поиск имени столбца с первичным ключом
            Dim PKColumnInfo = DI.ColumnInfo.FirstOrDefault(Function(ci) ci.PrimaryKey = True)

            If DI.ColumnInfo.Where(Function(ci) ci.PrimaryKey = True).Count() > 1 Then Throw New SQLPrimaryKeyTooManyException
            If PKColumnInfo Is Nothing Then Throw New SQLPrimaryKeyNotFoundException

            Dim strUpdateClause As New StringBuilder


            'Генерируем запрос обновления
            For Each xColumn In DI.ColumnInfo

                If xColumn.ColumnName <> PKColumnInfo.ColumnName Then
                    Dim ColumnInfo = DI.FindByColumnName(xColumn.ColumnName)
                    Dim xColumnValue = DI.ColumnValues(xColumn.ColumnName)

                    strUpdateClause.Append("[")
                    strUpdateClause.Append(xColumn.ColumnName)
                    strUpdateClause.Append("]=")

                    If xColumnValue Is Nothing Then
                        strUpdateClause.Append("null")
                    Else
                        Select Case ColumnInfo.ColumnType
                            Case StorageClass.Text
                                strUpdateClause.Append("'")
                                strUpdateClause.Append(xColumnValue.ToString())
                                strUpdateClause.Append("',")

                            Case StorageClass.Fractional, StorageClass.Integral
                                strUpdateClause.Append(xColumnValue.ToString())
                                strUpdateClause.Append(",")

                            Case StorageClass.Bool
                                strUpdateClause.Append("'")
                                strUpdateClause.Append(Convert.ToBoolean(xColumnValue).ToString().ToLowerInvariant())
                                strUpdateClause.Append("',")

                            Case StorageClass.DateTime
                                strUpdateClause.Append("'")
                                strUpdateClause.Append(Convert.ToDateTime(xColumnValue).ToString(SQL_DATE_FORMAT))
                                strUpdateClause.Append("',")
                        End Select
                    End If
                End If

            Next

            If strUpdateClause.Length > 0 Then
                strUpdateClause.Remove(strUpdateClause.Length - 1, 1)
            Else
                Throw New SQLColumnsNotFoundException("Нет столбцов или имеется только столбец первичного ключа. Обновление невозможно в таких таблицах")
            End If

            'Выполняем запрос к базе, создав запрос
            Dim SqlCn As IDbConnection = OpenConnection()
            Dim SqlCmd As IDbCommand = SqlCn.CreateCommand()

            SqlCmd.CommandText = String.Format("update [{0}] set {1} where [{2}] = {3}", DI.TableNameInfo, strUpdateClause.ToString, PKColumnInfo.ColumnName, DI.ColumnValues(PKColumnInfo.ColumnName))
            Return SqlCmd.ExecuteNonQuery()
        End Function

        ''' <summary>Удаление данных на основе данных класса. По свойству с атрибутом PrimaryKey происходит выборка удаляемых данных</summary>
        Public Function DeleteData(Of TClass)(Data As TClass) As Integer
            Dim DI = FromClassToDataInfo(Data)

            'Поиск имени столбца с первичным ключом
            Dim PKColumnInfo = DI.ColumnInfo.FirstOrDefault(Function(ci) ci.PrimaryKey = True)

            If DI.ColumnInfo.Where(Function(ci) ci.PrimaryKey = True).Count() > 1 Then Throw New SQLPrimaryKeyTooManyException
            If PKColumnInfo Is Nothing Then Throw New SQLPrimaryKeyNotFoundException

            'Выполняем запрос к базе, создав запрос
            Dim SqlCn As IDbConnection = OpenConnection()
            Dim SqlCmd As IDbCommand = SqlCn.CreateCommand()

            SqlCmd.CommandText = String.Format("delete from [{0}] where [{1}] = {2}", DI.TableNameInfo, PKColumnInfo.ColumnName, DI.ColumnValues(PKColumnInfo.ColumnName))
            Dim retInt As Integer = SqlCmd.ExecuteNonQuery()

            Return retInt
        End Function

#Region "       Поддержка IDisposable       "
        Private disposedValue As Boolean ' To detect redundant calls

        ' IDisposable
        Protected Overridable Sub Dispose(disposing As Boolean)
            If Not Me.disposedValue Then
                If disposing Then
                    ' >> dispose managed state (managed objects).
                    If Connection IsNot Nothing Then
                        RaiseEvent ClosingConnection(Connection)

                        Connection.Close()
                        Connection.Dispose()
                        Connection = Nothing
                    End If

                    GC.Collect()
                    GC.WaitForPendingFinalizers()
                End If

                ' >> free unmanaged resources (unmanaged objects) and override Finalize() below. set large fields to null.
            End If

            Me.disposedValue = True
        End Sub

        ' This code added by Visual Basic to correctly implement the disposable pattern.
        Public Sub Dispose() Implements IDisposable.Dispose
            ' Do not change this code.  Put cleanup code in Dispose(disposing As Boolean) above.
            Dispose(True)
            GC.SuppressFinalize(Me)
        End Sub
#End Region

    End Class

#End Region

End Namespace
